O objetivo deste desafio é criar um modelo preditivo calculando a probabilidade de inadimplência de cada novo pedido de crédito.
util_linhas_inseguras: Quanto que o cliente está usando, relativamente ao limite dele, de linhas de crédito que não são seguradas por qualquer bem do tomador e.g: imoveis, carros etc.
vezes_passou_de_30_59_dias: Número de vezes que o cliente atrasou, entre 30 e 59 dias, o pagamento de um empréstimo.
idade: A idade do cliente.
razao_debito: Razão entre as dívidas e o patrimônio do tomador. razão débito = Dividas/Patrimônio
salario_mensal: Salário mensal do cliente.
numero_linhas_crdto_aberto: Número de empréstimos em aberto pelo cliente.
numero_vezes_passou_90_dias: Número de vezes que o tomador passou mais de 90 dias em atraso.
numero_emprestimos_imobiliarios: Quantidade de empréstimos imobiliários que o cliente possui em aberto.
numero_de_vezes_que_passou_60_89_dias: Número de vezes que o cliente atrasou, entre 60 e 89 dias, o pagamento de um empréstimo.
numero_de_dependentes: O número de pessoas dependentes do cliente.
#importing usefulls libraries
import pandas as pd
import numpy as np
import plotly.express as px
import warnings
import os
from matplotlib import pyplot as plt
warnings.filterwarnings("ignore")
pwd = os.getcwd()
filepath = os.path.join(pwd, "treino.csv")
filepath
'C:\\Users\\James Bond\\Desktop\\Trabalho\\case_datarisk\\treino.csv'
treino = pd.read_csv(filepath)
treino.tail()
| inadimplente | util_linhas_inseguras | idade | vezes_passou_de_30_59_dias | razao_debito | salario_mensal | numero_linhas_crdto_aberto | numero_vezes_passou_90_dias | numero_emprestimos_imobiliarios | numero_de_vezes_que_passou_60_89_dias | numero_de_dependentes | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 109995 | 0 | 0.137396 | 59 | 1 | 0.448912 | 9600.0 | 10 | 0 | 2 | 0 | 0.0 |
| 109996 | 0 | 0.276964 | 46 | 0 | 0.491288 | 12224.0 | 19 | 0 | 4 | 0 | 4.0 |
| 109997 | 0 | 0.181257 | 43 | 0 | 0.382635 | 12000.0 | 13 | 0 | 2 | 0 | 2.0 |
| 109998 | 0 | 0.037699 | 86 | 0 | 0.248107 | 7000.0 | 14 | 0 | 2 | 0 | 1.0 |
| 109999 | 0 | 1.000000 | 57 | 0 | 0.002352 | 3825.0 | 1 | 0 | 0 | 0 | 0.0 |
treino.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 110000 entries, 0 to 109999 Data columns (total 11 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 inadimplente 110000 non-null int64 1 util_linhas_inseguras 110000 non-null float64 2 idade 110000 non-null int64 3 vezes_passou_de_30_59_dias 110000 non-null int64 4 razao_debito 110000 non-null float64 5 salario_mensal 88237 non-null float64 6 numero_linhas_crdto_aberto 110000 non-null int64 7 numero_vezes_passou_90_dias 110000 non-null int64 8 numero_emprestimos_imobiliarios 110000 non-null int64 9 numero_de_vezes_que_passou_60_89_dias 110000 non-null int64 10 numero_de_dependentes 107122 non-null float64 dtypes: float64(4), int64(7) memory usage: 9.2 MB
treino.describe()
| inadimplente | util_linhas_inseguras | idade | vezes_passou_de_30_59_dias | razao_debito | salario_mensal | numero_linhas_crdto_aberto | numero_vezes_passou_90_dias | numero_emprestimos_imobiliarios | numero_de_vezes_que_passou_60_89_dias | numero_de_dependentes | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 110000.000000 | 110000.000000 | 110000.000000 | 110000.000000 | 110000.000000 | 8.823700e+04 | 110000.000000 | 110000.000000 | 110000.000000 | 110000.000000 | 107122.000000 |
| mean | 0.066645 | 5.929979 | 52.255636 | 0.424055 | 354.823589 | 6.637411e+03 | 8.445573 | 0.269955 | 1.019891 | 0.243891 | 0.757482 |
| std | 0.249408 | 252.301965 | 14.768241 | 4.240780 | 2074.140421 | 1.338395e+04 | 5.139026 | 4.217326 | 1.135989 | 4.204137 | 1.114670 |
| min | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000e+00 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
| 25% | 0.000000 | 0.030054 | 41.000000 | 0.000000 | 0.175016 | 3.400000e+03 | 5.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
| 50% | 0.000000 | 0.155936 | 52.000000 | 0.000000 | 0.366682 | 5.400000e+03 | 8.000000 | 0.000000 | 1.000000 | 0.000000 | 0.000000 |
| 75% | 0.000000 | 0.562806 | 63.000000 | 0.000000 | 0.866874 | 8.225000e+03 | 11.000000 | 0.000000 | 2.000000 | 0.000000 | 1.000000 |
| max | 1.000000 | 50708.000000 | 109.000000 | 98.000000 | 329664.000000 | 3.008750e+06 | 58.000000 | 98.000000 | 54.000000 | 98.000000 | 20.000000 |
Vamos compreender melhor a distribuição dos dados, encontrar outliers e valores inconsistentes.
#Scatterplot matrix
fig = px.scatter_matrix(treino, dimensions=
["util_linhas_inseguras","idade","vezes_passou_de_30_59_dias","salario_mensal",'numero_linhas_crdto_aberto', 'numero_vezes_passou_90_dias',
'numero_emprestimos_imobiliarios','numero_de_vezes_que_passou_60_89_dias', 'numero_de_dependentes','razao_debito'],
labels={col:col.replace('_', ' ') for col in treino.columns}, height=900, color="inadimplente", color_continuous_scale=px.colors.diverging.Tealrose)
fig.show()
treino.hist(bins=40, figsize=(20, 15))
array([[<AxesSubplot:title={'center':'inadimplente'}>,
<AxesSubplot:title={'center':'util_linhas_inseguras'}>,
<AxesSubplot:title={'center':'idade'}>],
[<AxesSubplot:title={'center':'vezes_passou_de_30_59_dias'}>,
<AxesSubplot:title={'center':'razao_debito'}>,
<AxesSubplot:title={'center':'salario_mensal'}>],
[<AxesSubplot:title={'center':'numero_linhas_crdto_aberto'}>,
<AxesSubplot:title={'center':'numero_vezes_passou_90_dias'}>,
<AxesSubplot:title={'center':'numero_emprestimos_imobiliarios'}>],
[<AxesSubplot:title={'center':'numero_de_vezes_que_passou_60_89_dias'}>,
<AxesSubplot:title={'center':'numero_de_dependentes'}>,
<AxesSubplot:title={'center':'rendaPorDependente'}>]],
dtype=object)
#"util_linhas_inseguras","idade","vezes_passou_de_30_59_dias","salario_mensal",'numero_linhas_crdto_aberto', 'numero_vezes_passou_90_dias',
#'numero_emprestimos_imobiliarios','numero_de_vezes_que_passou_60_89_dias', 'numero_de_dependentes','razao_debito'
fig = px.box(treino, x="inadimplente", y="util_linhas_inseguras", color="inadimplente",
color_discrete_sequence=px.colors.qualitative.Dark24,
labels={col:col.replace('_', ' ') for col in treino.columns},
category_orders={})
fig.update_layout(legend=dict(orientation="h", yanchor="bottom",
y=1.02, xanchor="right", x=1))
fig.show()
treino.corr()
| inadimplente | util_linhas_inseguras | idade | vezes_passou_de_30_59_dias | razao_debito | salario_mensal | numero_linhas_crdto_aberto | numero_vezes_passou_90_dias | numero_emprestimos_imobiliarios | numero_de_vezes_que_passou_60_89_dias | numero_de_dependentes | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| inadimplente | 1.000000 | -0.003263 | -0.114281 | 0.124546 | -0.006534 | -0.020271 | -0.029332 | 0.116023 | -0.008144 | 0.101536 | 0.043079 |
| util_linhas_inseguras | -0.003263 | 1.000000 | -0.004444 | -0.001336 | 0.001255 | 0.008203 | -0.010120 | -0.001154 | 0.007590 | -0.001105 | -0.000986 |
| idade | -0.114281 | -0.004444 | 1.000000 | -0.063230 | 0.025221 | 0.040922 | 0.148640 | -0.061371 | 0.032920 | -0.057604 | -0.212194 |
| vezes_passou_de_30_59_dias | 0.124546 | -0.001336 | -0.063230 | 1.000000 | -0.006860 | -0.010939 | -0.056347 | 0.984157 | -0.031093 | 0.987377 | -0.003183 |
| razao_debito | -0.006534 | 0.001255 | 0.025221 | -0.006860 | 1.000000 | -0.033734 | 0.047158 | -0.008344 | 0.117054 | -0.007558 | -0.042631 |
| salario_mensal | -0.020271 | 0.008203 | 0.040922 | -0.010939 | -0.033734 | 1.000000 | 0.096490 | -0.013725 | 0.133508 | -0.012404 | 0.066444 |
| numero_linhas_crdto_aberto | -0.029332 | -0.010120 | 0.148640 | -0.056347 | 0.047158 | 0.096490 | 1.000000 | -0.080873 | 0.433337 | -0.071979 | 0.065191 |
| numero_vezes_passou_90_dias | 0.116023 | -0.001154 | -0.061371 | 0.984157 | -0.008344 | -0.013725 | -0.080873 | 1.000000 | -0.045902 | 0.993162 | -0.011052 |
| numero_emprestimos_imobiliarios | -0.008144 | 0.007590 | 0.032920 | -0.031093 | 0.117054 | 0.133508 | 0.433337 | -0.045902 | 1.000000 | -0.039987 | 0.122481 |
| numero_de_vezes_que_passou_60_89_dias | 0.101536 | -0.001105 | -0.057604 | 0.987377 | -0.007558 | -0.012404 | -0.071979 | 0.993162 | -0.039987 | 1.000000 | -0.011660 |
| numero_de_dependentes | 0.043079 | -0.000986 | -0.212194 | -0.003183 | -0.042631 | 0.066444 | 0.065191 | -0.011052 | 0.122481 | -0.011660 | 1.000000 |
import seaborn as sns
corr = treino.corr()
mask = np.triu(np.ones_like(corr, dtype=bool))
f, ax = plt.subplots(figsize=(11, 9))
cmap = sns.diverging_palette(230, 20, as_cmap=True)
sns.heatmap(corr, mask=mask, cmap=cmap, vmax=.3, center=0,
square=True, linewidths=.5, cbar_kws={"shrink": .5})
<AxesSubplot:>
#treino[(treino['vezes_passou_de_30_59_dias'] == 98)]
#Removing outliers
treino = treino[treino["salario_mensal"]<=100000]
treino = treino[treino["vezes_passou_de_30_59_dias"]<= 9]
treino = treino[treino["util_linhas_inseguras"]<= 2]
treino = treino[treino["idade"]> 0]
treino = treino[treino["numero_de_dependentes"]< 20]
treino = round(treino[treino["util_linhas_inseguras"]< 10], 1)
treino = round(treino[treino["razao_debito"]< 10], 1)
treino = treino[treino["numero_linhas_crdto_aberto"]< 50]
treino = treino[treino["numero_vezes_passou_90_dias"]< 20]
treino = treino[treino["numero_de_vezes_que_passou_60_89_dias"]< 6]
treino = treino[treino["numero_emprestimos_imobiliarios"]< 100]
treino = treino.reset_index(drop = True)
treino['salario_mensal'].fillna(float(treino['salario_mensal'].mean()), inplace=True)
treino['numero_de_dependentes'].fillna(int(treino['numero_de_dependentes'].mode()), inplace=True)
#treino = treino.dropna()
Após a limpeza é possível ver uma maior representatividade dos dados
treino["rendaPorDependente"] = treino["salario_mensal"] / (treino["numero_de_dependentes"]+1)
#treino["rendaPorLinhaCred"] = treino["salario_mensal"] / (treino["numero_linhas_crdto_aberto"]+1)
#Definindo Previsores e Classe
previsores = treino.iloc[:, 1:11].values
classe = treino.iloc[:, 0:1].values
from sklearn.model_selection import train_test_split
previsores_treinamento, previsores_teste, classe_treinamento, classe_teste = train_test_split(previsores, classe, test_size=0.2, random_state=0)
previsores_treinamento.shape, classe_treinamento.shape
previsores_teste.shape, classe_teste.shape
((21946, 10), (21946, 1))
print('Full database:\n',
treino["inadimplente"].value_counts() / len(treino),
'\n\nClasse Treinamento:\n',
pd.DataFrame(classe_treinamento)[0].value_counts() / len(classe_treinamento))
Full database: 0 0.933536 1 0.066464 Name: inadimplente, dtype: float64 Classe Treinamento: 0 0.933187 1 0.066813 Name: 0, dtype: float64
import xgboost as xgb
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression
from xgboost import XGBClassifier
from sklearn.metrics import classification_report
from sklearn.metrics import confusion_matrix
def model_assess(model, previsores_treinamento, classe_treinamento, previsores_teste, classe_teste, name='Default'):
model.fit(previsores_treinamento, classe_treinamento.ravel())
model_assess.preds_proba = model.predict_proba(previsores_treinamento)
print(confusion_matrix(classe_teste, model.predict(previsores_teste)),' ', name, '\n',
classification_report(classe_teste, model.predict(previsores_teste)))
#KNN
knn = KNeighborsClassifier(n_neighbors=151)
model_assess(knn,previsores_treinamento, classe_treinamento, previsores_teste, classe_teste, name='KNN')
[[20518 0]
[ 1428 0]] KNN
precision recall f1-score support
0 0.93 1.00 0.97 20518
1 0.00 0.00 0.00 1428
accuracy 0.93 21946
macro avg 0.47 0.50 0.48 21946
weighted avg 0.87 0.93 0.90 21946
#Logistic Regression
lg = LogisticRegression(random_state=0)
model_assess(lg,previsores_treinamento, classe_treinamento, previsores_teste, classe_teste, 'Logistic Regression')
[[20459 59]
[ 1359 69]] Logistic Regression
precision recall f1-score support
0 0.94 1.00 0.97 20518
1 0.54 0.05 0.09 1428
accuracy 0.94 21946
macro avg 0.74 0.52 0.53 21946
weighted avg 0.91 0.94 0.91 21946
#XGB
xgb = XGBClassifier(n_estimators=1000, learning_rate=0.05, eval_metric='mlogloss')
model_assess(xgb,previsores_treinamento, classe_treinamento, previsores_teste, classe_teste, 'XGBoost')
[[20314 204]
[ 1169 259]] XGBoost
precision recall f1-score support
0 0.95 0.99 0.97 20518
1 0.56 0.18 0.27 1428
accuracy 0.94 21946
macro avg 0.75 0.59 0.62 21946
weighted avg 0.92 0.94 0.92 21946
$Precision = TP/(TP+FP)$
$Recall = TP/(TP+FN)$
$F1 Score = (2*Precision*Recall)/(Precision+Recall)$
$Accuracy = (TP+TN) / (TP+TN+FP+FN)$
Observando a precisão vemos que o KNN não identificou nenhuma inadinplencia corretamente, possuindo o menor score.
Já o modelo de Logistic Regression e XGBoost possuem um desenpenho semelhante, conseguindo reconhecer corretamente eventos com inadinplencia, possuem score maior que seu base line.
Vamos agora observar a ROC, que é uma curva de probabilidade com Taxa de Falso Positivo (FPR) no eixo x e a Taxa de Verdadeiro Positivo (TPR) no eixo y.
from IPython.display import Image
Image(filename='ROC.jpeg')
from matplotlib import pyplot as plt
import sklearn.metrics as metrics
from sklearn.metrics import roc_auc_score
#ROC AUC
fig = plt.figure(figsize=(14,10))
plt.plot([0, 1], [0, 1],'r--')
#KNN
preds_proba_knn = knn.predict_proba(previsores_teste)
probsknn = preds_proba_knn[:, 1]
fpr, tpr, thresh = metrics.roc_curve(classe_teste, probsknn)
aucknn = roc_auc_score(classe_teste, probsknn)
plt.plot(fpr, tpr, label=f'KNN, AUC = {str(round(aucknn,3))}')
#Logistic Regression
preds_proba_lg = lg.predict_proba(previsores_teste)
probslg = preds_proba_lg[:, 1]
fpr, tpr, thresh = metrics.roc_curve(classe_teste, probslg)
auclg = roc_auc_score(classe_teste, probslg)
plt.plot(fpr, tpr, label=f'Logistic Regression, AUC = {str(round(auclg,3))}')
#XGBoost
preds_proba_xgb = xgb.predict_proba(previsores_teste)
probsxgb = preds_proba_xgb[:, 1]
fpr, tpr, thresh = metrics.roc_curve(classe_teste, probsxgb)
aucxgb = roc_auc_score(classe_teste, probsxgb)
plt.plot(fpr, tpr, label=f'XGBoost, AUC = {str(round(aucxgb,3))}')
plt.ylabel("True Positive Rate")
plt.xlabel("False Positive Rate")
plt.title("ROC curve")
plt.rcParams['axes.titlesize'] = 18
plt.legend()
plt.show()
O melhor modelo deve maximizar o TPR para 1 e minimizar o FPR para 0. Com isso, podemos comparar os classificadores usando a área sob a curva da curva ROC, onde quanto maior seu valor, melhor será o modelo em prever 0s como 0s e 1s como 1s.
Podemos ver que o XGBoost tem melhor desempenho, é o melhor classificador na distinção entre inadinplentes e não inadinplentes
Até agora, vimos a capacidade de cada modelo de prever "labels", agora avaliaremos seu desempenho ao prever a probabilidade da amostra pertencer à classe positiva, ou seja, probabilidade de inadimplência.
Dado que o Brier Score é uma função de custo, um Brier Score inferior indica uma previsão mais precisa.
Image(filename='BrierScore.png')
Em que Ft é a probabilidade prevista, Ot o resultado real do evento na instância temos o mean square error da previsão.
Esta formulação é usada principalmente para eventos binários (por exemplo, "inadinplencia" ou "sem inadinplencia").
from sklearn.calibration import calibration_curve
from sklearn.metrics import brier_score_loss
#Reliability plot and Brier Score
fig = plt.figure(figsize=(14,10))
plt.plot([0, 1], [0, 1], color="black")
#KNN
knn_y, knn_x = calibration_curve(classe_teste, preds_proba_knn[:,1], n_bins=10, normalize=True)
loss_knn = brier_score_loss(classe_teste, preds_proba_knn[:,1])
plt.plot(knn_x, knn_y, marker='o', label=f'KNN, Brier score = {str(round(loss_knn,3))}')
#Logistic Regression
lg_y, lg_x = calibration_curve(classe_teste, preds_proba_lg[:,1], n_bins=10, normalize=True)
loss_lg = brier_score_loss(classe_teste, preds_proba_lg[:,1])
plt.plot(lg_x, lg_y, marker='o',label=f'Logistic Regression, Brier score = {str(round(loss_lg,3))}')
#XGBoost
preds_proba_xgb = xgb.predict_proba(previsores_teste)
xgb_y, xgb_x = calibration_curve(classe_teste, preds_proba_xgb[:,1], n_bins=10, normalize=True)
loss_xgb = brier_score_loss(classe_teste, preds_proba_xgb[:,1])
plt.plot(xgb_x, xgb_y, marker='o', label=f'XGBoost, Brier score = {str(round(loss_xgb,3))}')
plt.ylabel("Actual probabilty")
plt.xlabel("Predicted probability")
plt.title("Reliability plot")
plt.rcParams['axes.titlesize'] = 18
plt.legend()
plt.show()
Mais uma vez vemos que XGBoost apresenta o menor Brier Score, indicando uma maior precisão do modelo.
Desta forma, é possível concluir que nosso modelo está bem calibrado para predição de probabilidade, o que significa que as probabilidades preditas se aproximam da distribuição esperada de probabilidades para cada classe.
teste = pd.read_csv("teste.csv")
teste = teste.dropna(axis=0)
teste = teste[teste["salario_mensal"]<=100000]
teste = teste[teste["vezes_passou_de_30_59_dias"]<=90]
teste = teste[teste["util_linhas_inseguras"]<= 11000]
teste = teste[teste["idade"]> 0]
teste = teste[teste["numero_de_dependentes"]< 20]
previsores = teste.iloc[:,:].values
preds = xgb.predict(previsores)
model_assess.preds_proba = xgb.predict_proba(previsores)
foo = pd.DataFrame(model_assess.preds_proba)
foo['inadinplente'] = 0
for i in range(len(foo)):
if (foo[0][i] >= 0.5):
foo['inadinplente'][i] = 0
else:
foo['inadinplente'][i] = 1
teste['inadinplente'] = foo['inadinplente']
teste.to_csv("classificatedTeste.csv")
Por curiosidade vemos a análise sobre relevância de cada atributo na classificação de inadiplência
from xgboost import plot_importance
#Feature importance plot
fig, (ax1) = plt.subplots(figsize = (15, 17))
plt.subplots_adjust(left=0.125, right=0.9, bottom=0.1, top = 0.9, wspace=0, hspace = 0.5)
plot_importance(xgb, importance_type='gain', ax = ax1)
ax1.set_title('Feature Importance by Information Gain', fontsize = 18)
ax1.set_xlabel('Gain')